package com.db.stexchem.client;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.db.stexchem.exceptions.BadRequestException;
import com.db.stexchem.message.ExecutionReport;
import com.db.stexchem.message.LoginRequest;
import com.db.stexchem.message.LoginResponse;
import com.db.stexchem.message.Message;
import com.db.stexchem.message.Order;
import com.db.stexchem.message.PlaceOrderResponse;
import com.db.stexchem.serialization.ProtoBufSerializerImpl;
import com.db.stexchem.serialization.Serializer;

public class ClientImpl implements Client {
  private final Logger logger = LoggerFactory.getLogger(ClientImpl.class);
  private volatile boolean connected = false;
  private Socket socket = new Socket();
  private NotificationListener listener;
  private BlockingQueue<PlaceOrderResponse> queue = new ArrayBlockingQueue<>(1);
  private Serializer serializer = new ProtoBufSerializerImpl();
  private Thread readThread;
  
  public ClientImpl() {
  }
  
  @Override
  public void connect(String host, int port, String login, String password)
      throws UnknownHostException, IOException, BadRequestException {
    if (connected) {
      throw new IllegalStateException("The client is already connected");
    }
    if (login == null) {
      throw new IllegalArgumentException(
          "login argument must be a non-null value");
    }
    if (password == null) {
      throw new IllegalArgumentException(
          "password argument must be a non-null value");
    }
    logger.trace("connecting to host '{}' on '{}' port ...", host, port);
    socket = new Socket(host, port);
    
    serializer.serialize(socket.getOutputStream(), new LoginRequest(login,
        password));
    
    LoginResponse response = serializer.deserializeLoginResponse(socket
        .getInputStream());
    switch (response.getStatus()) {
    case OK:
      logger.info("login operation with login '{}' has been successfully completed",
          login);
      break;
    case BAD_REQUEST:
    default:
      logger.error("failed to login client '{}':", login);
      try {
        socket.close();
      } catch (Exception e) { // dont'care
      }
      throw new BadRequestException(response.getErrorMessage());
    }
    connected = true;
    
    readThread = new Thread(new Runnable() {
      @Override
      public void run() {
        while (connected) {
          Message message;
          try {
            message = serializer.deserializeMessage(socket.getInputStream());
          } catch (IOException e) {
            logger.error(e.getMessage());
            connected = false;
            try {
              socket.close();
            } catch (IOException ex) { // dont'care
            }
            return;
          }
          switch (message.getMessageType()) {
          case PLACE_ORDER_RESPONSE:
            try {
              queue.put((PlaceOrderResponse) message.getData());
            } catch (InterruptedException e) { // dont'care
            }
            break;
          case EXECUTION_REPORT:
          default:
            ExecutionReport report = (ExecutionReport) message.getData();
            logger.info("received {}", report);
            listener.onNotificationReceived(report);
          }
        }
      }
    });
    readThread.start();
  }
  
  @Override
  public int placeOrder(Order order) throws IOException, BadRequestException {
    if (!connected) {
      throw new IllegalStateException("The client is not connected");
    }
    
    if (order == null) {
      throw new IllegalArgumentException(
          "order argument must be a non-null value");
    }
    logger.trace("trying to place order ...");
    serializer.serialize(socket.getOutputStream(), order);
    
    PlaceOrderResponse response = null;
    try {
      response = queue.poll(5L, TimeUnit.SECONDS);
    } catch (InterruptedException e) { // don't care
    }
    if (response == null) {
      close();
      throw new IOException("failed to receive place order response");
    }
    switch (response.getStatus()) {
    case OK:
      logger.info("order has been successfully placed with # '{}'", response.getOrderNo());
      return response.getOrderNo();
    case BAD_REQUEST:
    default:
      logger.error("failed to place order: {}", response.getErrorMessage());
      throw new BadRequestException(response.getErrorMessage());
    }
  }
  
  @Override
  public void setNotificationListener(NotificationListener listener) {
    if (listener == null) {
      throw new IllegalArgumentException(
          "listener argument must be a non-null value");
    }
    this.listener = listener;
  }
  
  @Override
  public void close() {
    if (!connected) {
      return;
    }
    logger.trace("closing socket ...");
    try {
      connected = false;
      socket.close();
      readThread.join();
    } catch (IOException e) { // don't care
    } catch (InterruptedException e) { // don't care
    }
  }
  
}
